﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Xml;
using Windows.ApplicationModel;
using Windows.Storage;
using Windows.Storage.Pickers;
using Windows.System;
using Windows.UI.Core;
using Windows.UI.ViewManagement;
using Windows.UI.Xaml.Media;
using LinkPad.Handlers;
using LinkPad.Helpers;
using LinkPad.Shapes;
using Windows.Foundation;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Input;
using Windows.UI.Xaml.Navigation;
using Windows.Storage.AccessCache;

// The Blank Page item template is documented at http://go.microsoft.com/fwlink/?LinkId=234238

namespace LinkPad
{
    /// <summary>
    /// An empty page that can be used on its own or navigated to within a Frame.
    /// </summary>
    public sealed partial class DiagramPage : IGlobalController
    {
        #region 

        private enum InputState
        {
            None = -1,
            Panning = 0,
            EnteringText,
            AddingImages,
            DrawingLinks,
            DrawingFreehand,
            Editing,
            Erasing
        }

        #endregion

        #region Fields

        private readonly ShapeManager _shapeManager;

        private readonly List<ICanvaActionHandler> _handlers = new List<ICanvaActionHandler>();

        private InputState _inputState = InputState.None;

        private IHasColor _currColorReceiver;
        private IHasFont _currFontReceiver;

        private StorageFile _file;

        private readonly CanvasPincher _pincher;

        private double _oldViewLeft;
        private double _oldViewTop;
        private double _oldViewWidth;
        private double _oldViewHeight;

        /// <summary>
        ///  Flag that instructs the program to re-fit-to-screen when the canvas content chnages
        /// </summary>
        private bool _toFit;

        private bool _enablePinching;


        #endregion

        #region Constructors

        public DiagramPage()
        {
            InitializeComponent();

            UpdatePageSize();

            // NOTE the canvas should now have a nonzero and meaningful size
            _shapeManager = new ShapeManager(MainCanvas);

            _handlers.Add(new PanHandler(_shapeManager));
            _handlers.Add(new TextCreator(_shapeManager));
            _handlers.Add(new ImageAdder(_shapeManager));
            _handlers.Add(new LinkCreator(_shapeManager));
            _handlers.Add(new InkPenDrawer(_shapeManager));
            var editor = new Editor(_shapeManager, this);
            editor.AddSubEditor(new LinkEditor(_shapeManager));
            editor.AddSubEditor(new TextMover(_shapeManager));
            editor.AddSubEditor(new ImageMover(_shapeManager));
            _handlers.Add(editor);
            _handlers.Add(new Eraser(_shapeManager));

            SizeChanged += PageSizeChanged;

            MainCanvas.PointerPressed += MainCanvas_OnPointerPressed;
            MainCanvas.PointerMoved += Maincanvas_OnPointerMoved;
            MainCanvas.PointerReleased += MainCanvas_OnPointerReleased;
            MainCanvas.PointerExited += MainCanvas_OnPointerExited;
            MainCanvas.Tapped += MainCanvasOnTapped;
            MainCanvas.DoubleTapped += MainCanvas_DoubleTapped;
            MainCanvas.PointerWheelChanged += MainCanvasOnPointerWheelChanged;
            EnableWheeling = true;

            MainCanvas.SizeChanged += MainCanvasOnSizeChanged;

            MainCanvas.LayoutUpdated += (sender, o) =>
            {
                if (!_toFit)
                {
                    return;
                }
                FitAll();
                _toFit = false;
            };

            _pincher = new CanvasPincher(MainCanvas);
            _pincher.TransformStaged += PincherOnTransformStaged;
            _pincher.TransformChanged += PincherOnTransformChanged;
            _enablePinching = true;

            // reference:
            // http://social.msdn.microsoft.com/Forums/en-US/56272bc6-6085-426a-8939-f48d71ab12ca/page-keydown-event-not-firing?forum=winappswithcsharp
            Window.Current.CoreWindow.KeyDown += CoreWindowOnKeyDown;

            RdPan.Click += (sender, args) =>
            {
                CurrentInputState = InputState.Panning;
                _pincher.EnableAll();   // TODO check the redundant handling?
            };
            RdText.Click += (sender, args) =>
            {
                CurrentInputState = InputState.EnteringText;
                _pincher.DisableAllButRButtonClick();
            };
            RdImage.Click += (sender, args) =>
            {
                CurrentInputState = InputState.AddingImages;
                _pincher.DisableAllButRButtonClick();
            };
            RdLink.Click += (sender, args) =>
            {
                CurrentInputState = InputState.DrawingLinks;
                _pincher.DisableAllButRButtonClick();
            };
            RdFreehand.Click += (sender, args) =>
            {
                CurrentInputState = InputState.DrawingFreehand;
                _pincher.DisableAllButRButtonClick();
            };
            RdEdit.Click += (sender, args) =>
            {
                CurrentInputState = InputState.Editing;
                _pincher.DisableAllButRButtonClick();
            };
            RdEraser.Click += (sender, args) =>
            {
                CurrentInputState = InputState.Erasing;
                _pincher.DisableAllButRButtonClick(); // maybe it's ok for eraser not to have this?
            };

            BtnZoomIn.Click += BtnZoomInOnClick;
            BtnZoomOut.Click += BtnZoomOutOnClick;
            BtnFitAll.Click += BtnFitAllOnClick;

            BtnZoomIn.DoubleTapped += ButtonDoubleTapConsumer;
            BtnZoomOut.DoubleTapped += ButtonDoubleTapConsumer;
            BtnFitAll.DoubleTapped += ButtonDoubleTapConsumer;

            CurrentInputState = InputState.None;

            // colour picker
            foreach (var child in ColorPickerGrid.Children)
            {
                var button = child as Button;
                if (button == null) continue;
                button.Click += ColorPickerButtonClicked;
            }

            // font-size setter
            for (var d = 8; d <= 64; d++)
            {
                System.Diagnostics.Debug.Assert(ListFontSizes.Items != null);
                ListFontSizes.Items.Add(d);
            }
            ListFontSizes.SelectionChanged += ListFontSizesOnSelectionChanged;

            // app bar events
            MainAppBar.Opened += MainAppBarOnOpened;

            BtnSave.Click += BtnSaveOnClick;
            BtnToMain.Click += BtnToMainOnClick;

            // references:
            // http://msdn.microsoft.com/en-us/library/windows/apps/hh464925.aspx#app_suspend
            Application.Current.Suspending += CurrentOnSuspending;
            ApplicationView.TerminateAppOnFinalViewClose = true;
        }

        #endregion

        #region Properties

        private InputState CurrentInputState
        {
            get { return _inputState; }
            set
            {
                if (_inputState == value) return;
                _inputState = value;
                UpdateState();
            }
        }

        private StorageFile File
        {
            get
            {
                return _file;
            }
            set
            {
                if (_file == value) return;
                _file = value;
                if (_file != null)
                {
                    AddToRecent(_file);
                }
            }
        }

        public bool EnableWheeling
        {
            get;
            set;
        }

        public bool EnablePinching
        {
            get
            {
                return _enablePinching;
            }
            set
            {
                if (_enablePinching == value) return;
                _enablePinching = value;
                if (value)
                {
                    _pincher.Attach();
                    _pincher.TransformStaged += PincherOnTransformStaged;
                    _pincher.TransformChanged += PincherOnTransformChanged;
                }
                else
                {
                    _pincher.Detach();
                    _pincher.TransformStaged -= PincherOnTransformStaged;
                    _pincher.TransformChanged -= PincherOnTransformChanged;
                }
            }
        }

        #endregion

        #region Methods

        #region Event handlers

        private void PageSizeChanged(object sender, SizeChangedEventArgs e)
        {
            UpdatePageSize();
        }

        private void MainCanvas_OnPointerPressed(object sender, PointerRoutedEventArgs e)
        {
            
        }

        private void Maincanvas_OnPointerMoved(object sender, PointerRoutedEventArgs e)
        {   
        }

        private void MainCanvas_OnPointerReleased(object sender, PointerRoutedEventArgs e)
        {   
        }

        private void MainCanvas_OnPointerExited(object sender, PointerRoutedEventArgs e)
        { 
        }

        private void MainCanvas_DoubleTapped(object sender, DoubleTappedRoutedEventArgs e)
        {
        }

        private void MainCanvasOnTapped(object sender, TappedRoutedEventArgs e)
        {
        }

        private void ButtonDoubleTapConsumer(object sender, DoubleTappedRoutedEventArgs e)
        {
            e.Handled = true;
        }

        private void PincherOnTransformStaged()
        {
            // sets the current view as old view so the new transform is based on this view
            _oldViewLeft = _shapeManager.ViewLeft;
            _oldViewTop = _shapeManager.ViewTop;
            _oldViewWidth = _shapeManager.ViewWidth;
            _oldViewHeight = _shapeManager.ViewHeight;

            // resets the transform so the new transform starts from here
            if (_pincher.CurrentCount == 0)
            {
                _pincher.ResetTransform();
            }
        }

        private void PincherOnTransformChanged(double k1, double k2, double d1, double d2)
        {
            var canvasWidth = MainCanvas.Width;
            var canvasHeight = MainCanvas.Height;

            // NOTE the pincher is based on canvas coordinates and the locations/offsets needs to be converted to view coordinates
            d1 *= _oldViewWidth/canvasWidth;
            d2 *= _oldViewHeight/canvasHeight;
            // NOTE (k1, k2, d1, d2) transforms a point from where it originally was to where it currently is on the canvas
            // so the view range should be obtained through the reversal of the transformation
            var kk1 = 1/k1;
            var kk2 = 1/k2;
            var dd1 = -d1*kk1;
            var dd2 = -d2*kk2;
            var newViewLeft = kk1 * _oldViewLeft + dd1;
            var newViewTop = kk2 * _oldViewTop + dd2;
            var newViewWidth = kk1 * _oldViewWidth;
            var newViewHeight = kk2 * _oldViewHeight;

            _shapeManager.ViewAt(newViewLeft, newViewTop, newViewWidth, newViewHeight);
        }

        private void ColorPickerButtonClicked(object sender, RoutedEventArgs e)
        {
            var button = sender as Button;
            System.Diagnostics.Debug.Assert(button != null && ColorPickerButton.Flyout != null);
            ColorPickerButton.Background = button.Background;
            ColorPickerButton.Flyout.Hide();
            var brush = button.Background as SolidColorBrush;
            if (_currColorReceiver != null && brush != null)
            {
                _currColorReceiver.MainColor = brush.Color;
            }
        }

        private void ListFontSizesOnSelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            if (_currFontReceiver == null) return;
            if (e.AddedItems.Count == 0) return;
            var selected = e.AddedItems[0];
            var textFontSize = Convert.ToString(selected);
            TextFontSize.Text = textFontSize;
            FontSizePickerButton.Content = string.Format("Font Size {0}", textFontSize);
            _currFontReceiver.FontSize = Convert.ToDouble(selected);
        }

        private void MainCanvasOnSizeChanged(object sender, SizeChangedEventArgs e)
        {
            var size = e.NewSize;
            UpdateUILayout(size);
        }

        private async void GradualZoom(double cx, double cy, double inc, int iterationCount)
        {
            for (;iterationCount>=0;iterationCount--)
            {
                Zoom(cx, cy, inc);
                await Task.Delay(5);
            }
        }

        private void BtnZoomInOnClick(object sender, RoutedEventArgs e)
        {
            var canvasWidth = MainCanvas.ActualWidth;
            var canvasHeight = MainCanvas.ActualHeight;

            var cx = canvasWidth*0.5;
            var cy = canvasHeight*0.5;

            GradualZoom(cx, cy, 0.99, 20);
        }

        private void BtnZoomOutOnClick(object sender, RoutedEventArgs e)
        {
            var canvasWidth = MainCanvas.ActualWidth;
            var canvasHeight = MainCanvas.ActualHeight;

            var cx = canvasWidth*0.5;
            var cy = canvasHeight*0.5;

            GradualZoom(cx, cy, 1.01, 20);
        }

        private void BtnSaveOnClick(object sender, RoutedEventArgs e)
        {
            // save as
            Save(true);
        }

        private async void BtnToMainOnClick(object sender, RoutedEventArgs e)
        {
            await SaveAnyway();
            Frame.Navigate(typeof (MainPage));
        }

        private void MainCanvasOnPointerWheelChanged(object sender, PointerRoutedEventArgs e)
        {
            if (!EnableWheeling)
            {
                return;
            }

            var point = e.GetCurrentPoint(MainCanvas);
            var pos = point.Position;
            var wheelDelta = point.Properties.MouseWheelDelta;

            var cx = pos.X;
            var cy = pos.Y;

            var zoomRate = Math.Pow(1.06, wheelDelta > 0 ? -1 : 1);
            Zoom(cx, cy, zoomRate);
        }

        private void CoreWindowOnKeyDown(CoreWindow sender, KeyEventArgs e)
        {
            if (e.VirtualKey == VirtualKey.Delete && CurrentInputState == InputState.Editing)
            {
                var handler = _handlers[(int)CurrentInputState];
                var editor = handler as Editor;
                if (editor != null)
                {
                    editor.DeleteAllSelected();
                }
            }
        }

        private void BtnFitAllOnClick(object sender, RoutedEventArgs routedEventArgs)
        {
            FitAll();
        }

        /// <summary>
        /// Invoked when this page is about to be displayed in a Frame.
        /// </summary>
        /// <param name="e">Event data that describes how this page was reached.  The Parameter
        /// property is typically used to configure the page.</param>
        protected override void OnNavigatedTo(NavigationEventArgs e)
        {
            var file = e.Parameter as StorageFile;
            if (file != null)
            {
                LoadFromFile(file);
                File = file;
            }
            
            base.OnNavigatedTo(e);
        }

        private void MainAppBarOnOpened(object sender, object o)
        {
            var i = (int) _inputState;
            if (i >= 0)
            {
                UpdateHandlerAppBarUI(i);                
            }
        }

        private async void CurrentOnSuspending(object sender, SuspendingEventArgs suspendingEventArgs)
        {
            await SaveAnyway();
        }

        #endregion

        private async void GradualViewTo(Rect targetRect, int numIntervals, int delayEachInterval)
        {
            var originalRect = new Rect(_shapeManager.ViewLeft, _shapeManager.ViewTop,
                _shapeManager.ViewWidth, _shapeManager.ViewHeight);

            var targetCentreX = (targetRect.Left + targetRect.Right) / 2;
            var targetCentreY = (targetRect.Top + targetRect.Bottom) / 2;
            var originCentreX = (originalRect.Left + originalRect.Right) / 2;
            var originCentreY = (originalRect.Top + originalRect.Bottom) / 2;

            var deltaX = (targetCentreX - originCentreX) / numIntervals;
            var deltaY = (targetCentreY - originCentreY) / numIntervals;
            var deltaW = (targetRect.Width - originalRect.Width) / numIntervals;
            var deltaH = (targetRect.Height - originalRect.Height) / numIntervals;

            for (var i = 1; i <= numIntervals; i++)
            {
                var x = originCentreX + deltaX * i;
                var y = originCentreY + deltaY * i;
                var w = originalRect.Width + deltaW * i;
                var h = originalRect.Height + deltaH * i;
                var left = x - w / 2;
                var top = y - h / 2;
                _shapeManager.ViewAt(left, top, w, h);
                await Task.Delay(delayEachInterval);
            }
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="cx"></param>
        /// <param name="cy"></param>
        /// <param name="scale">A value greater than 1 to zoom out</param>
        private void Zoom(double cx, double cy, double scale)
        {
            var oldViewLeft = _shapeManager.ViewLeft;
            var oldViewTop = _shapeManager.ViewTop;
            var oldViewWidth = _shapeManager.ViewWidth;
            var oldViewHeight = _shapeManager.ViewHeight;

            var canvasWidth = MainCanvas.ActualWidth;
            var canvasHeight = MainCanvas.ActualHeight;

            var dx = cx * oldViewWidth / canvasWidth;
            var dy = cy * oldViewHeight / canvasHeight;
            var absx = oldViewLeft + dx;
            var absy = oldViewTop + dy;
            var newViewLeft = absx - dx * scale;
            var newViewTop = absy - dy * scale;
            var newViewWidth = oldViewWidth * scale;
            var newViewHeight = oldViewHeight * scale;

            _shapeManager.ViewAt(newViewLeft, newViewTop, newViewWidth, newViewHeight);
        }

        private async void New()
        {
            await SaveAnyway();
            File = null;
            _shapeManager.Clear();
        }

        private async Task SaveAnyway()
        {
            if (_shapeManager.Shapes.Count == 0)
            {
                return;
            }
            await SetTempFile();
            if (File != null)
            {
                Save();
            }
        }

        private void AddToRecent(StorageFile file)
        {
            var list = StorageApplicationPermissions.MostRecentlyUsedList;
            list.Add(file);
        }

        private async Task SetTempFile()
        {
            if (File != null)
            {
                return;
            }
            var folder = ApplicationData.Current.TemporaryFolder;
            File = await folder.CreateFileAsync("temp.lnp", CreationCollisionOption.GenerateUniqueName);
        }

        private async void Save(bool newFile = false)
        {
            var oldFile = File;
            if (newFile || File == null)
            {
                if (UIHelper.EnsureUnsnapped())
                {
                    var savePicker = new FileSavePicker
                    {
                        SuggestedStartLocation = PickerLocationId.DocumentsLibrary,
                        SuggestedFileName = "New Document",
                    };
                    savePicker.FileTypeChoices.Add("LinkPad File", new List<string> { ".lnp" });

                    File = await savePicker.PickSaveFileAsync();
                }
                else
                {
                    // TODO notify the user
                    return;
                }
            }

            if (File == null) return;

            if (oldFile != null && oldFile.IsTempFile() && !File.IsTempFile())
            {
                oldFile.DeleteAsync();
            }

            // Application now has read/write access to all contents in the picked folder (including other sub-folder contents)
            CachedFileManager.DeferUpdates(File);
            var settings = new XmlWriterSettings { Indent = true };
            var sb = new StringBuilder();
            using (var xmlWriter = XmlWriter.Create(sb, settings))
            {
                _shapeManager.WriteToXml(xmlWriter);
            }

            FileIO.WriteTextAsync(File, sb.ToString());
        }

        private async void Load()
        {
            if (!UIHelper.EnsureUnsnapped())
            {
                // TODO notify the user
                return;
            }

            var filePicker = new FileOpenPicker
            {
                SuggestedStartLocation = PickerLocationId.DocumentsLibrary
            };
            filePicker.FileTypeFilter.Add(".lnp");
            var file = await filePicker.PickSingleFileAsync();

            if (file == null)
            {
                // TODO notify the user
                return;
            }

            File = file;

            LoadFromFile(file);
        }

        private void LoadFromFile(StorageFile file)
        {
            var textTask = FileIO.ReadTextAsync(file);

            var e = new AutoResetEvent(false);
            var text = "";

            textTask.Completed += (info, status) =>
            {
                text = info.GetResults();
                e.Set();
            };

            e.WaitOne();

            if (text == "")
            {
                return; // error
            }

            using (var textReader = new StringReader(text))
            {
                using (var xmlReader = XmlReader.Create(textReader))
                {
                    _shapeManager.ReadFromXml(xmlReader);
                }
            }

            FitAll();
            PushFitAll();
        }

        private void PushFitAll()
        {
            _toFit = true;
        }

        private void FitAspectRatio(ref Rect rect)
        {
            if (MainCanvas.ActualWidth*rect.Height < MainCanvas.ActualHeight*rect.Width)
            {
                // main canvas is narrower
                var targetHeight = rect.Width * MainCanvas.ActualHeight / MainCanvas.ActualWidth;
                var cy = (rect.Top + rect.Bottom) / 2;
                rect.Y = cy - targetHeight / 2;
                rect.Height = targetHeight;
            }
            else
            {
                // main cavas is wider
                var targetWidth = rect.Height * MainCanvas.ActualWidth / MainCanvas.ActualHeight;
                var cx = (rect.Left + rect.Right) / 2;
                rect.X = cx - targetWidth / 2;
                rect.Width = targetWidth;
            }
        }

        private void FitAll()
        {
            if (!_shapeManager.BoundariesReady)
            {
                PushFitAll();
                return;
            }
            var boundaries = _shapeManager.Boundaries;

            FitAspectRatio(ref boundaries);

            var expanded = boundaries.ExpandRect(1.1, 1.1);

            if (expanded.Width < double.Epsilon || expanded.Height < double.Epsilon)
            {
                expanded = new Rect(0, 0, MainCanvas.ActualWidth, MainCanvas.ActualHeight);
            }

            if (MainCanvas.ActualWidth < double.Epsilon || MainCanvas.ActualHeight < double.Epsilon)
            {
                MainCanvas.SizeChanged += RefitAll;
                return;
            }

            GradualViewTo(expanded, 20, 5);
        }

        private void RefitAll(object sender, SizeChangedEventArgs sizeChangedEventArgs)
        {
            MainCanvas.SizeChanged -= RefitAll;
            FitAll();
        }

        private void UpdatePageSize()
        {
            var screenWidth = Window.Current.Bounds.Width;
            var screenHeight = Window.Current.Bounds.Height;

            MainCanvas.Width = screenWidth;
            MainCanvas.Height = screenHeight;
        }

        private void UpdateState()
        {
            _currColorReceiver = null;
            _currFontReceiver = null;

            ColorPickerButton.Visibility = Visibility.Collapsed;
            FontSizePickerButton.Visibility = Visibility.Collapsed;

            for (var i = 0; i < _handlers.Count; i++)
            {
                if (i == (int)_inputState)
                {
                    _handlers[i].Activate();
                    UpdateHandlerAppBarUI(i);
                }
                else
                {
                    _handlers[i].Deactivate();
                }
            }
        }

        private void UpdateHandlerAppBarUI(int i)
        {
            var hasColor = _handlers[i] as IHasColor;
            if (hasColor != null && hasColor.HasColor)
            {
                _currColorReceiver = hasColor;
                ColorPickerButton.Visibility = Visibility.Visible;
                ColorPickerButton.Background = new SolidColorBrush(hasColor.MainColor);
            }
            else
            {
                _currColorReceiver = null;
                ColorPickerButton.Visibility = Visibility.Collapsed;
            }

            var hasFont = _handlers[i] as IHasFont;
            if (hasFont != null && hasFont.HasFont)
            {
                _currFontReceiver = hasFont;
                var size = hasFont.FontSize;
                FontSizePickerButton.Visibility = Visibility.Visible;
                FontSizePickerButton.Content = string.Format("Font Size {0}", size);
                TextFontSize.Text = Convert.ToString(size);
                // TODO select the corresponding list box item?
            }
            else
            {
                _currFontReceiver = null;
                FontSizePickerButton.Visibility = Visibility.Collapsed;
            }
        }

        private void UpdateUILayout(Size size)
        {
            // Nothing to do at the moment
        }

        #endregion
    }
}
